package com.simpou.commons.persistence.file;

import com.simpou.commons.persistence.common.Transaction;
import com.simpou.commons.persistence.common.exception.CommitFailedException;
import com.simpou.commons.persistence.common.exception.RollbackFailedException;
import com.simpou.commons.persistence.file.model.FileContent;
import com.simpou.commons.persistence.file.model.FileLocation;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

import lombok.Getter;


import com.simpou.commons.utils.file.FileHelper;
import com.simpou.commons.utils.lang.ArraysHelper;
import java.util.HashMap;
import java.util.Map;

/**
 * Gerencia arquivos em disco de forma transacionada, ou seja, se algum erro
 * ocorrer durante um commit de alterações é possível reverter o processo.
 *
 * @author Jonas Pereira
 * @since 2013-08-22
 */
public class TransactedFileManager {

    public final File baseFile;

    public TransactedFileManager(final File baseFile) {
        this.baseFile = baseFile;
        this.baseFile.mkdirs();
    }
    @Getter
    private XmlTransaction transaction = new XmlTransaction();

    public XmlTransaction newTransaction() {
        transaction = new XmlTransaction();
        return transaction;
    }

    public FileContent read(final FileLocation query) throws Exception {
        final String path = getPath(query);
        if (!FileHelper.exists(path)) {
            return null;
        }
        final String content = FileHelper.read(path);
        final FileContent xmlData = new FileContent(query, content.getBytes());
        return xmlData;
    }

    public List<FileLocation> list(final FileLocation query) throws Exception {
        final String path = getPath(query);
        if (!FileHelper.exists(path)) {
            return null;
        }
        final List<String> filePaths = FileHelper.list(path, false, null);
        final List<FileLocation> list = new ArrayList<FileLocation>(filePaths.size());
        for (final String filePath : filePaths) {
            final FileLocation xmlQuery = new FileLocation(query.getDir(), new File(
                    filePath).getName());
            list.add(xmlQuery);
        }
        return list;
    }

    private String getPath(final FileLocation query) {
        String path;
        if (query.getName() == null) {
            path = query.getDir();
        } else {
            path = query.getPath();
        }
        final File filePath = new File(this.baseFile, path);
        return filePath.getAbsolutePath();
    }

    public void delete(final FileLocation query) throws Exception {
        final Operation operation = new Operation(query, Operation.Type.DELETE);
        this.transaction.addOperation(operation);
    }

    private void _delete(final FileLocation query) throws Exception {
        final String path = getPath(query);
        final boolean delete = FileHelper.delete(path);
        if (!delete) {
            // TODO lançar uma melhor
            throw new Exception("File delete fail for " + path);
        }
    }

    public void write(final FileContent data) throws Exception {
        final Operation operation = new Operation(data, Operation.Type.WRITE);
        this.transaction.addOperation(operation);
    }

    private void _write(final FileContent data) throws Exception {
        final String path = getPath(data.getLocation());
        FileHelper.write(path, new String(data.getContent()), false, false);
    }

    private boolean exists(final FileLocation query) {
        final String path = getPath(query);
        return FileHelper.exists(path);
    }

    private class XmlTransaction implements Transaction {

        @Getter
        private boolean active;

        private final Map<FileLocation, Operation> mapOperations = new HashMap<FileLocation, Operation>();

        private final List<Operation> operations = new ArrayList<Operation>();

        private final List<Operation> successOpers = new ArrayList<Operation>();

        private void addOperation(final Operation operation) {
            synchronized (this.operations) {
                if (this.active) {
                    if (mapOperations.containsKey(operation.query)) {
                        final Operation operAdded = mapOperations.get(operation.query);
                        //não adiciona operações opostas
                        if ((operAdded.type.equals(Operation.Type.DELETE)
                                && operation.type.equals(Operation.Type.WRITE))
                                || (operAdded.type.equals(Operation.Type.WRITE)
                                && operation.type.equals(Operation.Type.DELETE))) {
                            mapOperations.remove(operation.query);
                            operations.remove(operAdded);
                            return;
                        }
                    }
                    this.operations.add(operation);
                    mapOperations.put(operation.query, operation);
                } else {
                    throw new IllegalStateException(
                            "Transaction is not active.");
                }
            }
        }

        @Override
        public void begin() {
            synchronized (this.operations) {
                if (this.active) {
                    throw new IllegalStateException(
                            "Transaction already activated.");
                } else {
                    this.active = true;
                    this.operations.clear();
                    this.successOpers.clear();
                }
            }
        }

        @Override
        public void commit() throws Exception {
            synchronized (this.operations) {
                if (this.active) {
                    for (Operation operation : operations) {
                        try {
                            final Operation operationBkp;
                            switch (operation.type) {
                                case WRITE:
                                    _write(operation.data);
                                    operationBkp = operation;
                                    break;
                                case DELETE:
                                    FileContent xmlDataBkp = read(operation.query);
                                    _delete(operation.query);
                                    operationBkp = new Operation(xmlDataBkp, Operation.Type.DELETE);
                                    break;
                                default:
                                    throw new UnsupportedOperationException("Unsupported operation type: " + operation.type);
                            }
                            this.successOpers.add(operationBkp);
                        } catch (final Exception ex) {
                            rollback();
                            throw new CommitFailedException(ex, "Commit fail, an automatic rollback was called.");
                        }
                    }
                    this.active = false;
                } else {
                    throw new IllegalStateException(
                            "Transaction is not active.");
                }
            }
        }

        @Override
        public void rollback() {
            synchronized (this.operations) {
                if (this.active) {
                    StackTraceElement[] stackTraces = new StackTraceElement[0];
                    for (final Operation operation : successOpers) {
                        try {
                            switch (operation.type) {
                                case DELETE:
                                    if (!exists(operation.query)) {
                                        _write(operation.data);
                                    }
                                    break;
                                case WRITE:
                                    if (exists(operation.query)) {
                                        _delete(operation.query);
                                    }
                                    break;
                            }
                        } catch (final Exception ex) {
                            final StackTraceElement[] stackTrace = ex
                                    .getStackTrace();
                            stackTraces = ArraysHelper.concat(stackTraces,
                                    stackTrace);
                        }
                    }
                    this.active = false;
                    if (stackTraces.length > 0) {
                        throw new RollbackFailedException(stackTraces);
                    }
                }
            }
        }
    }

    private static class Operation {

        private final FileContent data;

        private final FileLocation query;

        private final Type type;

        private Operation(FileContent data, Type type) {
            this.data = data;
            this.type = type;
            this.query = data.getLocation();
        }

        private Operation(FileLocation query, Type type) {
            this.query = query;
            this.type = type;
            this.data = null;
        }

        private enum Type {

            WRITE, DELETE;

        }
    }
}
